# -*- coding: utf8 -*-

## Servidor de mails promiscuo y de juguete -- TdC - FCEN - UBA ##
#
# Disclaimer:
# ~~~~~~~~~~
#
# Este servidor es incompleto y NO implementa la especificación de SMTP (RFCs 821/2821/5321).
# El código que sigue debajo se divulga en el marco del taller de aplicación del
# día 27/06/2012, de manera que los alumnos puedan continuar y profundizar
# el aprendizaje modificándolo según prefieran.
#
 
from SocketServer import TCPServer, StreamRequestHandler
from collections import defaultdict


class ToyMailServer(TCPServer):
    def __init__(self, address = '', port = 25):
        # Lo que sigue es para poder reutilizar el puerto si cerramos el servidor y volvemos a ejecutarlo enseguida.
        self.allow_reuse_address = True
        # Un objeto TCPServer, provisto por la implementación de Python, nos permite escuchar en un puerto TCP
        # arbitrario e interactuar 'transparentemente' con la contraparte que inicia una conexión.
        # La clase que recibe como segundo argumento (en este caso, MailRequestHandler) es la que se debe 
        # encargar de manejar la comunicación, leyendo y enviando los datos que correspondan. 
        TCPServer.__init__(self, (address, port), MailRequestHandler)
 
 
class MailRequestHandler(StreamRequestHandler):
    def __init__(self, request, client_address, obj):
        self.initialize_messages()
        self.initialize_command_parsers()        
        StreamRequestHandler.__init__(self, request, client_address, obj)       
        
    def initialize_messages(self):
        # El siguiente diccionario define los distintos mensajes que el servidor podrá enviar al cliente.
        # Esto dependerá de la acción que tome el cliente en cada caso.
        self.messages = dict()
        self.messages['announcement'] = '220 Ready and waiting.'
        self.messages['goodbye'] = '221 Chau master!'
        self.messages['greeting'] = '250 Hola %s. Encantado. Vamos a lo nuestro...'
        self.messages['sender confirmation'] = '250 Manda %s. Me parece bien.'
        self.messages['rcpt confirmation'] = '250 Recibe %s. Si, si, creo que lo conozco... dale gas!'
        self.messages['mail received'] = '250 Mail recibido. Sera entregado a la brevedad.'
        self.messages['data confirmation'] = '354 Listo el pollo. Pasame la data.'
        self.messages['unknown command'] = '500 Mi no entender.'
        self.messages['bad sequence'] = '503 Se ruega respetar el orden de los comandos.'
        
    def initialize_command_parsers(self):
        # Este diccionario mapea comandos SMTP a métodos para procesar el texto recibido del cliente.
        # Esto se debe a que los argumentos de cada comando son distintos entre sí.
        self.command_parsers = defaultdict(lambda: self.default_parser)
        self.command_parsers['HELO'] = self.helo_parser
        self.command_parsers['MAIL'] = self.mail_parser
        self.command_parsers['RCPT'] = self.rcpt_parser
        self.command_parsers['DATA'] = self.data_parser
        self.command_parsers['QUIT'] = self.quit_parser
        
    # Este método es el que realiza el trabajo principal.
    # Comienza a ejecutarse una vez recibida una conexión.
    def handle(self):
        try:
            # Lo primero que debemos hacer es anunciarnos al cliente.
            self.send_message('announcement')
            
            # Luego el cliente debe mandar el HELO...
            client_data = self.get_command('HELO')
            
            # ...y nosotros debemos entonces darle la cordial bienvenida.
            self.send_message('greeting', argument = client_data)
            
            # El cliente tiene que decirnos ahora de quién es el mail.
            sender = self.get_command('MAIL')
            
            # Y nosotros, promiscuos, lo confirmamos sin miramientos.
            self.send_message('sender confirmation', argument = sender)
            
            # Ahora viene el destinatario (notar que potencialmente podría aparecer más de uno acá, pero no contemplamos este caso)
            rcpt = self.get_command('RCPT')
            
            # Y claramente decimos que no hay problema, sea cual sea ese destinatario!
            self.send_message('rcpt confirmation', argument = rcpt)
            
            # El cliente tiene que decir que está listo para transmitir el mensaje.
            self.get_command('DATA')
            
            # Y le decimos que le dé para adelante.
            self.send_message('data confirmation')
            
            # Lo que sigue recibe el texto completo enviado por el cliente.
            mail_text = self.get_mail()
            
            # Informamos al cliente la recepción exitosa de su correo.
            self.send_message('mail received')
            
            # Esperamos que el cliente cierre la conexión.
            self.get_command('QUIT')
            
            # Finalmente, nos despedimos de nuestro efímero amigo.
            self.send_message('goodbye')
            
            # En lo que queda, armamos un objeto para representar el mail y lo procesamos
            mail = Mail.from_text(mail_text)
            self.process(mail)
        except Exception, e:
            # Si en medio del diálogo ocurre algún imprevisto manifestado en excepción, nos despedimos.
            self.send_message('goodbye')
    
    ###  Parsers de comandos ###
    def default_parser(self, args):
        return None, None
    
    def helo_parser(self, args):
        return 'HELO', args[0]
        
    def mail_parser(self, args):
        args = args[0].split(':')
        from_keyword = args[0].upper()
        if from_keyword != 'FROM':
            return None, None
        address = args[1][1:-1]
        return 'MAIL', address
        
    def rcpt_parser(self, args):
        args = args[0].split(':')
        to_keyword = args[0].upper()
        if to_keyword != 'TO':
            return None, None
        address = args[1][1:-1]
        return 'RCPT', address
          
    def data_parser(self, args):
        return 'DATA', args   
    
    def quit_parser(self, args):
        return 'QUIT', args
    
    # Método para enviar los mensajes al cliente.
    def send_message(self, message_name, argument = None):
        message = self.messages[message_name]
        if argument:
            message = message % argument
        self.wfile.write(message + '\r\n')         
        
    def parse_input(self):
        input_data = self.rfile.readline().strip().split(' ')
        command = input_data[0].upper()
        args = input_data[1:]
        return self.command_parsers[command](args)
        
    def get_command(self, command_name):
        command, data = self.parse_input()
        if command not in self.command_parsers:
            # Si no podemos parsear el comando, le avisamos al cliente que lo desconocemos. 
            self.send_message('unknown command')
            # Vamos a volver a esperar el comando que tenemos que recibir.
            return self.get_command(command_name)        
        if command != command_name:
            # Si por algún motivo nos llega un QUIT, salimos inmediatamente.
            if command == 'QUIT':
                raise Exception()
            # Para mantener la historia simple, devolvemos un error de secuencia de comandos inválida
            # si lo que viene es conocido pero no es lo esperado. Observar que en la vida real esto no 
            # necesariamente es así.
            self.send_message('bad sequence')
            return self.get_command(command_name)
        return data
            
    def get_mail(self):
        mail_text = str()
        line = self.rfile.readline().strip()      
        # Según la especificación, el texto finaliza con una línea conteniendo sólo un punto.
        # Observar que el cliente debe hacer dot-stuffing!
        while line != '.':
            # Acá hacemos el reverso del stuffing mencionado arriba.
            if line[:2] == '..':
                line = line[1:]
            mail_text += line + '\n'
            line = self.rfile.readline().strip()  
        return mail_text
        
    def process(self, mail):
        # Acá podemos hacer cualquier cosa con el mail recibido:
        # Guardarlo en una BD, analizarlo, alterarlo y reenviarlo, etc...
        print 'Mirá lo que recibí! %s' % mail
        
        
class Mail(object):
    @classmethod
    def from_text(cls, text):
        parts = text.split('\n\n')
        header = parts[0]
        content = parts[1][:-1]
        return cls(header, content)
 
    def __init__(self, header, content):
        self.header = header
        self.content = content
        
    def get_header(self):
        return self.header
        
    def get_content(self):
        return self.content
       
    def __repr__(self):
        to_address_start = self.header.find('To:') + 4
        to_address_end = self.header.find('\n', to_address_start)
        to_address = self.header[to_address_start : to_address_end]
        from_address_start = self.header.find('From:') + 6
        from_address_end = self.header.find('\n', from_address_start)
        from_address = self.header[from_address_start : from_address_end]
        content = self.content
        return '<mail para %s de %s diciendo <%s>>' % (to_address, from_address, content)
        


if __name__ == "__main__":
    smtpd = ToyMailServer()
    smtpd.serve_forever()
